// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;

using Tierless.Framework.Data;
using Tierless.Framework.Constraints;
using Tierless.Framework.EntityTypes;

namespace Tierless.Framework.Entities
{
	/// <summary>
	/// Represents a specific entity of a given type.
	/// </summary>
	[DataContract]
	#if !SILVERLIGHT
	public class Entity : INotifyPropertyChanged, ICustomTypeDescriptor, ICloneable
	#else
	public class Entity : INotifyPropertyChanged, ICloneable
	#endif
	{
		/// <summary>
		/// Initializes a new entity.
		/// </summary>
		/// <param name="entityTypeName">The type of the entity.</param>
		public Entity(string entityTypeName)
		{
			if (String.IsNullOrEmpty(entityTypeName)) throw new ArgumentNullException("AEntityTypeName");

			EntityTypeName = entityTypeName;

			_lookups = new EntityLookups(this);
			_children = new EntityChildren(this);
		}

		/// <summary>
		/// Gets the name of the entity type.
		/// </summary>
		[DataMember]
		public string EntityTypeName { get; private set; }

		private EntityType _entityType;
		/// <summary>
		/// Gets or sets the EntityType instance describing the type of the entity.
		/// </summary>
		public EntityType EntityType
		{
			get
			{
				// TODO: This will need to be re-designed in order to support multiple databases in a single application
				return _entityType ?? (_entityType = EntityTypeRepository.EntityTypeProvider.GetEntityType(EntityTypeName));
			}
			set
			{
				if (value != null && value.Name != EntityTypeName)
					throw new InvalidOperationException(String.Format("Entity type name '{0}' does not match the expected entity type for this entity '{1}'.", value.Name, EntityTypeName));
				_entityType = value;
			}
		}

		/// <summary>
		/// Gets or sets the entity context for the entity.
		/// </summary>
		public EntityContext EntityContext { get; set; }

		private EntityColumnCollection _columns;

		/// <summary>
		/// Gets the set of columns for the entity.
		/// </summary>
		public EntityColumnCollection Columns
		{
			get
			{
				if (_columns == null)
					CreateEntityColumns();
				return _columns;
			}
		}

		/// <summary>
		/// Gets the entity column of the given name.
		/// </summary>
		/// <param name="columnName">The name of the column to be retrieved.</param>
		/// <returns>The entity column with the given name.</returns>
		public EntityColumn this[string columnName]
		{
			get { return Columns[columnName]; }
		}

		/// <summary>
		/// Gets the ID for the entity.
		/// </summary>
		public EntityID ID
		{
			get
			{
				// TODO: Cache this...
				return new EntityID(this, EntityType.PrimaryKey);
			}
		}

		/// <summary>
		/// Returns true if the entity type is temporal, false otherwise.
		/// </summary>
		public bool IsTemporal
		{
			get { return EntityType.IsTemporal; }
		}

		private int? _fromDateColumnIndex;

		protected int GetFromDateColumnIndex()
		{
			if (!_fromDateColumnIndex.HasValue)
				_fromDateColumnIndex = Columns.IndexOf(EntityType.FromDateColumnName);
			return _fromDateColumnIndex.Value;
		}

		/// <summary>
		/// Gets the DateTimeColumn representing the FromDate for the entity.
		/// </summary>
		public DateTimeColumn FromDateColumn
		{
			get
			{
				if (!EntityType.IsTemporal)
					throw new InvalidOperationException(String.Format("{0} does not have a from date column.", EntityType.Name));
				return (DateTimeColumn) Columns[GetFromDateColumnIndex()];
			}
		}

		/// <summary>
		/// Gets or sets the FromDate for the entity.
		/// </summary>
		public DateTime FromDate
		{
			get { return FromDateColumn.AsDateTime; }
			set { FromDateColumn.AsDateTime = value; }
		}

		private int? _toDateColumnIndex;

		protected int GetToDateColumnIndex()
		{
			if (!_toDateColumnIndex.HasValue)
				_toDateColumnIndex = Columns.IndexOf(EntityType.ToDateColumnName);
			return _toDateColumnIndex.Value;
		}

		/// <summary>
		/// Gets the DateTimeColumn representing the ToDate for the entity.
		/// </summary>
		public DateTimeColumn ToDateColumn
		{
			get
			{
				if (!EntityType.IsTemporal)
					throw new InvalidOperationException(String.Format("{0} does not have a to date column.", EntityType.Name));
				return (DateTimeColumn) Columns[GetToDateColumnIndex()];
			}
		}

		/// <summary>
		/// Gets or sets the ToDate for the entity.
		/// </summary>
		public DateTime? ToDate
		{
			get { return ToDateColumn.IsNull ? (DateTime?) null : ToDateColumn.AsDateTime; }
			set
			{
				if (value.HasValue)
					ToDateColumn.AsDateTime = value.Value;
				else
					ToDateColumn.Clear();
			}
		}

		[DataMember]
		private EntityChildren _children;

		/// <summary>
		/// Gets the list of entity sets describing the children for this entity.
		/// </summary>
		public EntityChildren Children
		{
			get { return _children; }
		}

		[DataMember]
		private EntityLookups _lookups;

		/// <summary>
		/// Gets the entities describing the lookups for this entity.
		/// </summary>
		public EntityLookups Lookups
		{
			get { return _lookups; }
		}

		[DataMember]
		private EntityState _state = EntityState.New;

		/// <summary>
		/// Gets the current state of this entity.
		/// </summary>
		public EntityState State
		{
			get { return _state; }
		}

		/// <summary>
		/// Gets whether or not this entity is new.
		/// </summary>
		public bool IsNew
		{
			get { return (_state & EntityState.New) != 0; }
		}

		/// <summary>
		/// Gets whether or not this entity is modified.
		/// </summary>
		public bool IsModified
		{
			get { return (_state & EntityState.Modified) != 0; }
		}

		/// <summary>
		/// Gets whether or not this entity is deleted.
		/// </summary>
		public bool IsDeleted
		{
			get { return (_state & EntityState.Deleted) != 0; }
		}

		/// <summary>
		/// Gets whether or not this entity has outstanding changes (i.e. is new, modified, or deleted.)
		/// </summary>
		public bool IsDirty
		{
			get { return _state != EntityState.Unchanged; }
		}

		protected bool InternalIsRelevant()
		{
			return EntityType.IsRelevant(this);
		}

		/// <summary>
		/// Returns true if this entity has enough information to be considered relevant.
		/// </summary>
		public bool IsRelevant
		{
			get { return InternalIsRelevant(); }
		}

		internal bool IsPopulating { get; private set; }

		/// <summary>
		/// Begins population mode on the entity, allowing values to be set without changing the entity state.
		/// </summary>
		public void BeginPopulate()
		{
			IsPopulating = true;
			EnsureCurrentData();
		}

		/// <summary>
		/// Ends population mode on the entity and sets the state to unchanged.
		/// </summary>
		public void EndPopulate()
		{
			IsPopulating = false;
			_state = EntityState.Unchanged;
		}

		[DataMember]
		private object[] _originalData;

		[DataMember]
		private object[] _currentData;

		[DataMember]
		private object[] _computedData;

		private void EnsureCurrentData()
		{
			if (_currentData == null)
			{
				if (IsPopulating)
				{
					_currentData = new object[EntityType.ColumnTypes.Count];
					_computedData = new object[EntityType.ColumnTypes.Count];
				}
				else
				{
					_currentData = InternalGetDefaultData();
					_computedData = (object[]) _currentData.Clone();
					for (var i = 0; i < 0; i++)
					{
						if (EntityType.ColumnTypes[i].IsComputed)
						{
							_currentData[i] = null;
						}
					}
				}
			}
		}

		/// <summary>
		/// Ensures that if the entity is in the new state, it has the new record called, and the state correctly set.
		/// </summary>
		public virtual void EnsureNew()
		{
			if (IsNew)
			{
				OnNewRecord();
				_state = EntityState.New;
			}
		}

		public event EntityDelegate NewRecord;

		protected virtual void OnNewRecord()
		{
			if (NewRecord != null)
				NewRecord(this);
		}

		protected virtual object[] InternalGetDefaultData()
		{
			return EntityType.DefaultData(this);
		}

		protected internal object InternalGetValue(int columnIndex)
		{
			EnsureCurrentData();
			var columnType = EntityType.ColumnTypes[columnIndex];
			if (columnType.IsComputed)
			{
				_computedData[columnIndex] = columnType.GetComputedValue(this, _computedData[columnIndex]);
				return _computedData[columnIndex];
			}
			return _currentData[columnIndex];
		}

		protected internal object InternalGetOriginalValue(int columnIndex)
		{
			if (_originalData == null || _entityType.ColumnTypes[columnIndex].IsComputed)
				return InternalGetValue(columnIndex);
			return _originalData[columnIndex];
		}

		protected internal bool InternalSetValue(int columnIndex, object tempValue)
		{
			var isComputed = EntityType.ColumnTypes[columnIndex].IsComputed;
			if (IsPopulating)
			{
				if (isComputed)
					_computedData[columnIndex] = tempValue;
				else
					_currentData[columnIndex] = tempValue;
			}
			else
			{
				EnsureCurrentData();
				EnsureOriginalData();
				var columnName = Columns[columnIndex].Name;
				var currentValue = isComputed ? _computedData[columnIndex] : _currentData[columnIndex];
				if (Equals(currentValue, tempValue))
					return false;

				DoColumnChanging(columnIndex, currentValue, tempValue);
				if (isComputed)
				{
					_computedData[columnIndex] = tempValue;
				}
				else
				{
					_currentData[columnIndex] = tempValue;
					_state |= EntityState.Modified;
				}
				DoColumnChanged(columnIndex, currentValue, tempValue);
				DoPropertyChanged(columnName);

				if (isComputed)
					EntityType.ColumnTypes[columnIndex].SetComputedValue(this, tempValue);
				NotifyComputedColumnsChanged(columnName);
				return true;
			}
			return false;
		}

		protected static object[] CopyData(object[] data)
		{
			var copy = new object[data.Length];
			for (var index = 0; index < data.Length; index++)
			{
				if (data[index] != null)
				{
					if (data[index] is ValueType)
						copy[index] = data[index];
					else
					{
						var cloneable = data[index] as ICloneable;
						if (cloneable == null)
							throw new ArgumentException(String.Format("Type '{0}' cannot be used in an Entity because it is a reference type that does not implement the ICloneable interface.", data[index].GetType().Name));

						copy[index] = cloneable.Clone();
					}
				}
			}
			return copy;
		}

		/// <summary>
		/// Returns a new entity with the same current data as this entity.
		/// </summary>
		/// <returns>A new entity with the same current data as this entity.</returns>
		/// <remarks>
		/// The newly returned entity has the same data as the current data for this entity,
		/// and is in the unchanged state, regardless of the current state of this entity.
		/// This operation does not change the state of this entity.
		/// </remarks>
		public Entity Copy()
		{
			var result = new Entity(EntityTypeName) { EntityType = EntityType };
			if (_currentData != null)
				result._currentData = CopyData(_currentData);
			result._state = EntityState.Unchanged;
			return result;
		}

		protected void EnsureOriginalData()
		{
			if (_originalData == null)
			{
				for (var index = 0; index < EntityType.ColumnTypes.Count; index++)
					if (EntityType.ColumnTypes[index].IsComputed)
						_computedData[index] = EntityType.ColumnTypes[index].GetComputedValue(this, _computedData[index]);
				_originalData = CopyData(_currentData);
			}
		}

		protected void RestoreOriginalData()
		{
			if (_originalData != null)
			{
				_currentData = _originalData;
				_originalData = null;
			}
		}

		protected void ClearOriginalData()
		{
			_originalData = null;
		}

		/// <summary>
		/// Accepts the changes to this entity.
		/// </summary>
		/// <remarks>
		/// This method clears the original data for the entity, sets the state to unchanged, and removes any deleted children.
		/// </remarks>
		public void AcceptChanges()
		{
			var deletedEntities = new List<Entity>();
			foreach (var entitySet in Children)
			{
				deletedEntities.Clear();
				foreach (var entity in entitySet)
					if (entity.IsDeleted)
						deletedEntities.Add(entity);
				foreach (var entity in deletedEntities)
					entitySet.Remove(entity);
			}
			_state &= ~(EntityState.Modified | EntityState.New);
			if (IsDeleted)
				RemoveDeleteMark();
			ClearOriginalData();
		}

		/// <summary>
		/// Accepts the changes to this entity and all children.
		/// </summary>
		public void AcceptAllChanges()
		{
			Recurse(AEntity => AEntity.AcceptChanges());
		}

		/// <summary>
		/// Cancels any changes to this entity.
		/// </summary>
		/// <remarks>
		/// This method restores the original data for the entity, and sets the state to unchanged.
		/// </remarks>
		public void CancelChanges()
		{
			RestoreOriginalData();
			_state &= ~(EntityState.Modified | EntityState.New);
			if (IsDeleted)
				RemoveDeleteMark();
		}

		/// <summary>
		/// Cancles any changes to this entity and all children.
		/// </summary>
		public void CancelAllChanges()
		{
			Recurse(AEntity => AEntity.CancelChanges());
		}

		/// <summary>
		/// Marks the entity for deletion, with the given effective range for deletion.
		/// </summary>
		/// <param name="fromDate">The from date for the deletion.</param>
		/// <param name="toDate">The to date for the deletion, if any.</param>
		public void MarkToDelete(DateTime fromDate, DateTime? toDate)
		{
			_state |= EntityState.Deleted;
			if (IsTemporal)
			{
				FromDate = fromDate;
				ToDate = toDate;
			}
		}

		/// <summary>
		/// Marks the entity for deletion, as of the given date.
		/// </summary>
		/// <param name="fromDate">The from date for the deletion.</param>
		public void MarkToDelete(DateTime fromDate)
		{
			MarkToDelete(fromDate, null);
		}

		/// <summary>
		/// Marks the entity for deletion as of today.
		/// </summary>
		public void MarkToDelete()
		{
			MarkToDelete(DateTime.Today, null);
		}

		/// <summary>
		/// Clears the delete mark.
		/// </summary>
		public void RemoveDeleteMark()
		{
			_state &= ~EntityState.Deleted;
		}

		private void CreateEntityColumns()
		{
			_columns = new EntityColumnCollection();
			EntityTypeColumn columnType;
			for (var index = 0; index < EntityType.ColumnTypes.Count; index++)
			{
				columnType = EntityType.ColumnTypes[index];
				_columns.Add(columnType.CreateColumn(this, index));
			}
		}

		/// <summary>
		/// Fires whenever the value of a column is changing.
		/// </summary>
		public event EntityColumnChangeEventHandler ColumnChanging;

		protected void DoColumnChanging(int columnIndex, object currentValue, object newValue)
		{
			var column = Columns[columnIndex];
			column.ColumnType.OnColumnChanging(this, column, currentValue, newValue);
			if (ColumnChanging != null)
				ColumnChanging(this, column, currentValue, newValue);
		}

		/// <summary>
		/// Fires whenever the value of a column has changed.
		/// </summary>
		public event EntityColumnChangeEventHandler ColumnChanged;

		protected void DoColumnChanged(int columnIndex, object oldValue, object newValue)
		{
			var column = Columns[columnIndex];
			column.ColumnType.OnColumnChanged(this, column, oldValue, newValue);
			if (ColumnChanged != null)
				ColumnChanged(this, column, oldValue, newValue);
		}

		protected void NotifyComputedColumnsChanged(string triggeringColumnName)
		{
			foreach (var column in Columns.Where(AColumn => AColumn.ColumnType.IsComputed && AColumn.Name != triggeringColumnName))
			{
				DoPropertyChanged(column.Name);
			}
		}

		[DataMember]
		private ConstraintViolations _violations;

		/// <summary>
		/// Gets the list of constraint violations for this entity.
		/// </summary>
		public ConstraintViolations Violations
		{
			get { return _violations ?? (_violations = new ConstraintViolations()); }
		}

		#region INotifyPropertyChanged Members

		/// <summary>
		/// Fires whenever a property has changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		public void DoPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}

		#endregion
		
		#region ICustomTypeDescriptor Members

		#if !SILVERLIGHT

		AttributeCollection ICustomTypeDescriptor.GetAttributes()
		{
			return TypeDescriptor.GetAttributes(this, true);
		}

		string ICustomTypeDescriptor.GetClassName()
		{
			return TypeDescriptor.GetClassName(this, true);
		}

		string ICustomTypeDescriptor.GetComponentName()
		{
			return TypeDescriptor.GetComponentName(this, true);
		}

		TypeConverter ICustomTypeDescriptor.GetConverter()
		{
			return TypeDescriptor.GetConverter(this, true);
		}

		EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
		{
			return TypeDescriptor.GetDefaultEvent(this, true);
		}

		PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
		{
			return TypeDescriptor.GetDefaultProperty(this, true);
		}

		object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
		{
			return TypeDescriptor.GetEditor(this, editorBaseType, true);
		}

		EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
		{
			return TypeDescriptor.GetEvents(this, attributes, true);
		}

		EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
		{
			return ((ICustomTypeDescriptor) this).GetEvents(null);
		}

		PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
		{
			return EntityPropertyDescriptorCache.GetPropertyDescriptors(EntityType, Columns);
		}

		PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
		{
			return ((ICustomTypeDescriptor) this).GetProperties(null);
		}

		object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor descriptor)
		{
			return this;
		}
		
		#endif

		#endregion

		#region Recursive Methods

		/// <summary>
		/// Executes the given delegate on this entity, and each child entity, recursively.
		/// </summary>
		/// <param name="callback"></param>
		public void Recurse(EntityDelegate callback)
		{
			if (callback != null)
			{
				callback(this);

				foreach (var set in Children)
					foreach (var childEntity in set)
						childEntity.Recurse(callback);
			}
		}

		/// <summary>
		/// Gets whether or not the entity, or any of its children, recursively, has any outstanding changes that need to be saved.
		/// </summary>
		public bool ShouldSave
		{
			get
			{
				if ((IsNew && IsRelevant) || (!IsNew && IsModified))
					return true;

				if (!IsNew && IsDeleted)
					return true;

				foreach (var set in Children)
					foreach (var childEntity in set)
						if (childEntity.ShouldSave)
							return true;

				return false;
			}
		}

		/// <summary>
		/// Gets whether or not the entity, or any of its children recursively, has any violations.
		/// </summary>
		public bool AnyViolations
		{
			get
			{
				if (Violations.Count > 0)
					return true;

				foreach (var set in Children)
					foreach (var childEntity in set)
						if (childEntity.AnyViolations)
							return true;

				return false;
			}
		}

		/// <summary>
		/// Gets whether or not the entity, or any of its children recursively, has any errors.
		/// </summary>
		public bool AnyErrors
		{
			get
			{
				if (Violations.HasErrors())
					return true;

				foreach (var set in Children)
					foreach (var childEntity in set)
						if (childEntity.AnyErrors)
							return true;

				return false;
			}
		}

		/// <summary>
		/// Detaches the given column changed handler from this entity and all its children, recursively.
		/// </summary>
		/// <param name="columnChangedHandler">The column changed handler to be detached.</param>
		public void DetachColumnChanged(EntityColumnChangeEventHandler columnChangedHandler)
		{
			Recurse(delegate(Entity ASubentity) { ASubentity.ColumnChanged -= columnChangedHandler; });
		}

		/// <summary>
		/// Attaches the given column changed handler to this entity and all its children, recursively.
		/// </summary>
		/// <param name="columnChangedHandler">The column changed handler to be attached.</param>
		public void AttachColumnChanged(EntityColumnChangeEventHandler columnChangedHandler)
		{
			Recurse(delegate(Entity ASubentity) { ASubentity.ColumnChanged += columnChangedHandler; });
		}

		/// <summary>
		/// Detaches the given collection changed event handler from this entity and its children, recursively.
		/// </summary>
		/// <param name="collectionChangedEventHandler">The handler to be detached.</param>
		public void DetachChildrenChangedRecursively(NotifyCollectionChangedEventHandler collectionChangedEventHandler)
		{
			Recurse(ASubentity => ASubentity.DetachChildrenChanged(collectionChangedEventHandler));
		}

		/// <summary>
		/// Attaches the given collection changed event handler to this entity and all its children, recursively.
		/// </summary>
		/// <param name="collectionChangedEventHandler">The handler to be attached.</param>
		public void AttachChildrenChangedRecursively(NotifyCollectionChangedEventHandler collectionChangedEventHandler)
		{
			Recurse(ASubentity => ASubentity.AttachChildrenChanged(collectionChangedEventHandler));
		}

		/// <summary>
		/// Detaches the given collection changed event handler from each child collection in this entity.
		/// </summary>
		/// <param name="collectionChangedEventHandler">The event handler to be detached.</param>
		public void DetachChildrenChanged(NotifyCollectionChangedEventHandler collectionChangedEventHandler)
		{
			foreach (var child in Children)
				child.CollectionChanged -= collectionChangedEventHandler;
		}

		/// <summary>
		/// Attaches the given collection changed event handler to each child collection in this entity.
		/// </summary>
		/// <param name="collectionChangedEventHandler">The event handler to be attached.</param>
		public void AttachChildrenChanged(NotifyCollectionChangedEventHandler collectionChangedEventHandler)
		{
			foreach (var child in Children)
				child.CollectionChanged += collectionChangedEventHandler;
		}

		/// <summary>
		/// Gathers constraint violations from this entity and all its children, recursively.
		/// </summary>
		/// <returns>The list of all constraint violations for this entity and all its children, recursively.</returns>
		public ConstraintViolations GatherViolations()
		{
			var violations = new ConstraintViolations();
			GatherViolations(violations);
			return violations;
		}

		/// <summary>
		/// Adds constraints for this entity and all its children, recursively, to the given list of constraint violations.
		/// </summary>
		/// <param name="violations">The list to contain the constraint violations.</param>
		public void GatherViolations(ConstraintViolations violations)
		{
			Recurse
			(
				delegate(Entity ASubEntity)
				{
					if (ASubEntity._violations != null)
						violations.AddRange(ASubEntity.Violations);
				}
			);
		}

		/// <summary>
		/// Clones this entity, including current data, computed data, original data, all children, lookups, and state.
		/// </summary>
		/// <returns>A deep clone of this entity, and all its children, recursively.</returns>
		public Entity Clone()
		{
			var result = new Entity(EntityTypeName) { EntityType = EntityType };

			if (_currentData != null)
				result._currentData = CopyData(_currentData);

			if (_computedData != null)
				result._computedData = CopyData(_computedData);

			if (_originalData != null)
				result._originalData = CopyData(_originalData);

			result.BeginPopulate();
			try
			{
				foreach (var entry in _lookups.Entries)
					result._lookups[entry.Key] = entry.Value;

				foreach (var entry in _children.Entries)
				{
					var childSet = result._children[entry.Key];
					foreach (var child in entry.Value)
						childSet.Add(child.Clone());
				}
			}
			finally
			{
				result.EndPopulate();
			}

			result._state = _state;

			return result;
		}

		/// <summary>
		/// Clones this entity, including current data, computed data, original data, all children, lookups, and state.
		/// </summary>
		/// <returns>A deep clone of this entity, and all its children, recursively.</returns>
		object ICloneable.Clone()
		{
			return Clone();
		}

		#endregion

		public override string ToString()
		{
			var result = new StringBuilder();
			result.AppendFormat("EntityType : System.String ({0})", EntityTypeName);
			for (var index = 0; index < Columns.Count; index++)
				result.AppendFormat("; {0}", Columns[index]);
			return result.ToString();
		}
	}
}